import json
import os
import sys
import threading
import time
import datetime
import paho.mqtt.client as mqtt
from fastapi import FastAPI
from fastapi.requests import   Request
#from fastapi_offline import FastAPIOffline
import uvicorn
import requests
import redis
'''
api: https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html
'''

def b2s( x ):
    return ('00'+hex(x)[2:])[-2:]
def bytes2str( bytes ):
    return "".join([ b2s(x) for x in bytes])
class TMqtt:

    def __init__(self):
        self.settings={
            "clientId":"",
            "host":"",
            "port":0,
            "uid":"",
            "pwd":""
        }
        self.client=  mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
        self.subTopics=[
            'zu/#'
        ]
        self.onConnectSuccess= None
        self.onConnectFail = None
        self.onDisConnect = None
        self.onMsg=None
        self.forceClosed = False
    def connect(self):
        res = {
            "status" :0
        }
        try:
            self.forceClosed = False
            self.disconnect()
            clientId =  self.settings['clientId']
            if clientId=='':
                clientId = 'mq-'+str(int(1000*time.time()))
                self.settings['clientId'] = clientId
            host =  self.settings['host']
            port =  self.settings['port']
            uid =  self.settings['uid']
            pwd =  self.settings['pwd']
            self.client=  mqtt.Client(mqtt.CallbackAPIVersion.VERSION2 , client_id= clientId)
            self.client.on_connect = self.client.on_connect
            self.client.on_connect_fail = self.on_connect_fail
            self.client.on_disconnect = self.on_disconnect
            self.client.on_message = self.on_message
            self.client.username_pw_set(uid , pwd )
            code = self.client.connect(host, port, 60)
            if code ==0 :
                for topic in self.subTopics:
                    self.client.subscribe(topic)
            print(f'connect to mqtt server.{host}:{port}.[{code}]')
            self.client.loop_start()
            res['status']=1
        except Exception as er:
            print(er)
        return res
    def disconnect(self):
        res = {
            "status" :0
        }
        try:
            self.forceClosed = True
            if( self.client!=None):
                if self.client.is_connected():
                    self.disconnect()
                self.client = None
        except Exception as er:
            print(er)
        return res
    def on_connect(self , client, userdata, flags, reason_code, properties):
        try:
            if reason_code.is_failure:
                print(f"Failed to connect: {reason_code}. loop_forever() will retry connection")
                if(self.onConnectFail!=None):
                    self.onConnectFail(reason_code)
            else:
                # 应该始终在 on_connect 回调中订阅以确保在重新连接时订阅依旧存在。
                print('---------  connect mqtt server success!  ---------------')
                if self.client!=None and self.client.is_connected():
                    for topic in self.subTopics:
                        self.client.subscribe(topic)
                if(self.onDisConnect!=None):
                    self.onConnectSuccess()
                print('-------------------------------------------------------')
        except Exception as er:
            print(er)
    def on_connect_fail(self , client, userdata):
        try:
            print('---------  connect mqtt server fail!  ---------------')
            if(self.onConnectFail!=None):
                self.onConnectFail(-1)
        except Exception as er:
            print(er)
    def on_disconnect(self , client, userdata, disconnect_flags, reason_code, properties):
        try:
            print('---------  disconnect from mqtt fail!  ---------------')
            print(f'disconnect_flags:{disconnect_flags}, reason_code:{reason_code}, properties:{properties}')
            print('-------------------------------------------------------')
            if not self.forceClosed:
                time.sleep(5000)
                self.client.reconnect()
            if(self.onDisConnect!=None):
                self.onDisConnect(reason_code)
        except Exception as er:
            print(er)
    def on_message(self , client, userdata, message):
        try:
            if not self.onMsg==None:
                #print('r->', message.topic ,  [hex(x) for x in message.payload])
                self.onMsg( message)
        except Exception as er:
            print(er)

class TRedisClient:
    def __init__(self):
        self.settings={
            "host":"iot.zn-power.com",
            "port":30015,
            "db":2
        }
        self.redisClient = None
    def writeRedisHash(self , dataPath , dictData):
        try:
            if self.redisClient==None:
                ps={
                    "host":self.settings['host'],
                    "port":self.settings['port'],
                    "db":self.settings['db']
                }
                r = redis.StrictRedis(**ps)
                if r.ping():
                    self.redisClient = r
                    print('connect redis success.')
                else:
                    print('connect redis faild.')
            if self.redisClient !=None:
                self.redisClient.hmset(dataPath , dictData)
        except Exception as er:
            print('&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&')
            print('write redis error.',er)
            print('key:' , dataPath)
            print('data:' , dictData)
            print('&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&')
class TTDBClient:
    def __init__(self):
        self.settings={
            "tdbUrl":"",
            "tdbToken":"",
            "tdbDB":""
        }
    def writePoints (self , tableName, points):
        try:
            url = self.settings['tdbUrl']
            db = self.settings['tdbDB']
            token = self.settings['tdbToken']
            repolicy = ''
            dataList=[]
            for pt in points:
                tagList = [ f"{x}={pt['tags'][x]}"  for x in pt['tags'].keys()]
                tags = ",".join(tagList)
                valueList = [ f"{x}={pt['values'][x]}"  for x in pt['values'].keys()]
                values = ",".join(valueList)
                dataList.append(f"{tableName},{tags} {values}")
            content = "\n".join(dataList)
            dataList.clear()
            dataList = None
            headers = {
                "Authorization": token,
                'Content-Type': 'application/json',
            }
            influxdb_url = f"{url}/write?db={db}"
            if repolicy != '':
                influxdb_url = influxdb_url + '&rp='+ repolicy
            resp = requests.post(url=influxdb_url, headers=headers, data=content)

            if str(resp.status_code) == '204' or str(resp.status_code) == '200':
                pass
            else:
                print(resp.content)
        except Exception as er:
            print(er)
class TDtu:
    def __init__(self):
        self.apiUrl = "https://iot.zn-power.com/zweb/api/znapi"
        #self.apiUrl = "http://127.0.0.1:12001/znapi"
        self.settings ={}
        self.info ={
            "dtus":{},
            "vars":{}
        }
        self.running = False
        self.dtuStatus = {}
        self.varValues = {}
        self.addrValueBuf ={}
        self.client = TMqtt()
        self.client.onConnectSuccess = self.onClient_connect
        self.client.onDisConnect = self.onClient_disconnect
        self.client.onConnectFail = self.onClient_connect_fail
        self.client.onMsg = self.onClient_message

        self.redisClient = TRedisClient()

        self.tdbClient = TTDBClient()

        self.logInfo = {}
        self.logTm = 0
        self.logs =[]
    def run(self):
        try:
            self.load()
            mqJson = self.settings['mqInfo']
            redisJson = self.settings['redisInfo']
            tdbJson = self.settings['tdbInfo']
            self.redisClient.settings = json.loads(redisJson)
            self.tdbClient.settings = json.loads(tdbJson)
            self.client.settings = json.loads(mqJson)
            self.client.connect()
            ( threading.Thread(target=self.sendMsg , daemon=True)).start()
        except Exception as er:
            print(er)
    def getCmds(self):
        res =[]
        try:
            def getCmdBytes (addr , option):
                res =[0x68,   0,0,0,0,0,0, 0x68 , 0x11 , 0x04 , 0,0,0,0 ]
                try:
                    addr = ('000000000000'+addr)[-12:]
                    offset = 1
                    for i in range(6):
                        ad = addr[12-i*2-2: 12-i*2 ]
                        #ad = addr[i*2:i*2+2]
                        adBytes = int(ad , base=16)
                        res[offset] = adBytes
                        offset = offset + 1
                    offset = offset + 3
                    size = int(0.5*len(option))
                    for i in range(size):
                        oi =0x33 +  int(option[6-i*2:2+6-i*2],base=10)
                        #oi =0x33 +  int(option[i*2:2+i*2],base=10)
                        res[offset] = oi
                        offset = offset + 1
                    crc = 0
                    for i in range(len(res)):
                        crc = crc + res[i]
                        crc = crc % 256
                    res.append(crc)
                    res.append(0x16)
                except Exception as e:
                    print(e)
                return bytearray(res)
                #return bytearray([0xfe ,0xfe , 0xfe ]+res)
            for item in self.info['vars'].values():
                addr = item['devCode']
                option = item['varCode'][-8:]
                cmdBytes = getCmdBytes(addr , option)
                topic = item['topicDown']
                delayTm = item['delayTm']
                res.append({
                    "varCode": item['varCode'],
                    "topic": topic,
                    "delayTm": delayTm ,
                    "cmdBytes" : cmdBytes,
                    "lastTm":0,
                    "var": item
                })

        except Exception as er:
            print(er)
        return res
    def sendMsg(self):
        try:
            cmds = self.getCmds()
            flag =  len(cmds)>0 and self.client.client!=None
            while flag:
                '''
                flag = len(cmds) > 0 and self.client.client != None
                if not flag :
                    break
                '''
                try:
                    for cmd in cmds:
                        time.sleep(5)
                        try:
                            lastTm = cmd['lastTm']
                            delayTm = cmd['delayTm']
                            cmdBytes = cmd['cmdBytes']
                            topic = cmd['topic']
                            tmNow = time.time()
                            dltTm = tmNow - lastTm
                            if dltTm > delayTm:
                                cmd['lastTm'] = tmNow
                                self.client.client.publish( topic , cmdBytes ,qos=0 , retain=False)
                                #self.addrValueBuf[cmd['var']['devCode']] = cmd['var']
                                #print('s->', topic , [hex(x) for x in cmdBytes])
                                print('s->', topic , bytes2str(cmdBytes))
                        except Exception as eer:
                            print(eer)
                except Exception as eer:
                    print(eer)
                    break
            print(len(cmds) >0, self.client.client != None , self.client.client.is_connected())
            self.client.disconnect()
            time.sleep(5)
            if len(cmds)>0:
                self.run()
        except Exception as er:
            print(er)
    def load(self):
        try:
            _url = f'{self.apiUrl}/db/query'
            ps= {
                "code":"dtu2025",
                "key": "config",
                "params":{}
            }
            respData = requests.post(  _url , json= ps).json()

            for item in respData['datasetList'][0]:
                self.settings[item['pk']] = item['pv']
            for item in respData['datasetList'][1]:
                self.info['dtus'][item['dtuId']] = item
            for item in respData['datasetList'][3]:
                self.info['vars'][item['varCode']] = item
            print(self.info)

            self.logInfo = json.loads(self.settings['logInfo'])
        except Exception as er:
            print(er)
    def getValue (self , value , dec , r1 ,r2 , negativeFlg = 1):
        res = 0
        try:
            v = int (value)
            if negativeFlg!=1 :
                if v >= negativeFlg:
                    v =-1 *  int((str(v))[1:])
            v = v * r1 * r2
            if dec>0:
                vs = str(v)
                pos = vs.find(".")
                if pos>=0:
                    v0 = vs[:pos]
                    v1 =vs[pos+1:] +'0000'
                    v1 = v1[:dec]
                    res = float(f"{v0}.{v1}")
                else:
                    res = v
            else:
                res = v
        except Exception as er:
            print(er)
        return  res

    def notify2Mqtt (self , var , value):
        try:
            devCode = var['devCode']
            varId = str(var['varId'])
            topic = f"zn/dtu/real/{devCode}/{varId}"
            msg={
                "flag":"zn_dtu_real",
                "devCode": var['devCode'],
                "varId": var['varId'],
                "varCode": var['varCode'],
                "varName": var['varName'],
                "tm":int(time.time()),
                "value":value,
                "unit":var['unit']
            }
            msgContent = json.dumps(msg, ensure_ascii=False)
            self.client.client.publish(topic , msgContent , 0  )
        except Exception as er:
            print(er)
    def post2TDB (self , var , value):
        try:
            tableName = 'd'+var['devCode']
            point = {
                "tags":{
                    "vid": str(var['varId'])
                },
                "values":{
                    "v": value
                }
            }
            self.tdbClient.writePoints(tableName , [point])
        except Exception as er:
            print(er)
    def updateRedis (self , var , value):
        try:
            tk = f"zn_dtu_tm/{var['devCode']}"
            tm = {}
            tm[str(var['varId'])] = int(time.time())
            self.redisClient.writeRedisHash(tk , tm)
            vk = f"zn_dtu_value/{var['devCode']}"
            val ={}
            val[str(var['varId'])] = value
            self.redisClient.writeRedisHash(vk , val)
        except Exception as er:
            print(er)

    def uploadVarvalues (self , var , value):
        try:
            self.post2TDB(var , value)
            self.updateRedis(var , value)
            self.notify2Mqtt(var , value)
        except Exception as er:
            print(er)
    def saveLog (self , bytes):
        try:
            logItem =  " ".join([('00'+hex(x)[2:])[-2:] for x in bytes ])
            self.logs.append({
                "tm": int(time.time()),
                "data":logItem
            })
            logInfo = self.logInfo
            tmThreshold = logInfo['timeThreshold']
            dlt = time.time() - self.logTm
            if dlt>tmThreshold:
                logPath = f"{logInfo['path']}/{datetime.datetime.now().strftime('%Y_%m_%d')}"
                if not os.path.exists(logPath):
                    os.makedirs(logPath,777)
                logFn = f"{logPath}/{datetime.datetime.now().strftime('%H')}"
                content = "\n".join([str(x['tm'])+'|'+x['data'] for x in self.logs])
                content = content+"\n"
                with open( logFn , 'a' , encoding='utf-8') as f:
                    f.write(content)
                    f.flush()
                self.logTm =int(time.time())
                self.logs.clear()
        except Exception as er:
            print(er)

    def onClient_connect(self ):
        try:
            pass
        except Exception as er:
            print(er)
    def onClient_connect_fail(self , reason_code ):
        try:
            pass
        except Exception as er:
            print(er)
    def onClient_disconnect(self , reason_code):
        try:
            pass
        except Exception as er:
            print(er)
    def onClient_message(self ,  message):
        try:
            recBytes = message.payload
            if len(recBytes)>=11 and recBytes[-1]==0x16:
                self.saveLog(recBytes)
                offset = recBytes.find(0x68)
                if offset>=0:
                    offset = offset + 1
                    addrBytes = recBytes[offset:offset+6]
                    addrList = list(addrBytes)
                    addrList.reverse()
                    addr = "".join([b2s(x) for x in addrList])
                    offset = offset + 6
                    offset = offset + 1
                    cf = recBytes[offset]
                    successFlag = (cf & 0x80 >0 )and(cf & 0x40 ==0)
                    if successFlag:
                        offset = offset + 1
                        dataSize = recBytes[offset]
                        offset = offset + 1
                        if dataSize>0:
                            dataBytes = recBytes[offset:offset + dataSize]
                            datas = list(dataBytes)

                            dataList = ["{:0>2d}".format(int(hex(x - 0x33).replace('0x', ''))) for x in datas]
                            dataList.reverse()
                            value = "".join([('00'+str(x))[-2:] for x in dataList])
                            itemCode = value[-8:]
                            dataValue = value[:-8]
                            varKey = f"{addr}_{itemCode}"

                            if varKey in self.info['vars'].keys():
                                var = self.info['vars'][varKey]
                                dec= var['decSize']
                                r1 = var['rate1']
                                r2 = var['rate2']
                                varCode = addr+'_'+itemCode
                                #var = self.varValues[varCode]
                                varItem = var['varItem']
                                flag = 1
                                if varItem in ['02020100','02020200','02020300','02030000','02040000']:
                                    print(dataValue , itemCode)
                                    flag =800000
                                elif varItem in ['02060000']:
                                    flag= 1000

                                v = self.getValue(dataValue , dec , r1 , r2 , flag)
                                msg = f"{var['varCode']}->{var['varName']}: {v} {var['unit']} "
                                print(msg)
                                #print(var['varCode'] , var['varName'] ,bytes2str(recBytes) , dataValue, value ,'/', var['rate2'],'=', v, var['unit'])
                                print('----------------------------------------------------')
                                self.uploadVarvalues(var , v )


                    else:
                        print('data input fail.')





        except Exception as er:
            print(er)


app = FastAPI()
@app.get("/")
async def appRoot():
    return {
        'start app zongneng iot server 2025'
    }
@app.get("/dtu/status")
async def appRoot():
    return {
        "dtu": "dtu.dtuStatus",
        "var": "dtu.varValues"
    }
@app.get("/jsonCmd")
async def jsonCmd(req:Request ):
    res ={}
    ps= {}
    for k,v in req.query_params.items():
        ps[k] = v
    for k,v in req.path_params.items():
        ps[k] = v
    for k , v in ps.items():
        pos = k.find("$json")
        if pos>0:
            try:
                objK = k[:pos]
                res[objK] = json.loads(v)
            except Exception as eer:
                print(eer)
        else:
            res[k] = v

    return res
@app.get("/cmd")
async def cmd(req:Request ):
    res ={}
    ps= {}
    for k,v in req.query_params.items():
        ps[k] = v
    def assignPs( parent , k , v ):
        pos = k.find("$")
        if pos<0:
            parent[k] = v
        else:
            pk=k[:pos]
            sk = k[pos+1:]
            if pk not in parent.keys():
                parent[pk]={}
            np = parent[pk]
            assignPs(np , sk , v )
    for k,v in ps.items():
        assignPs(res , k , v)

    return res
if __name__=="__main__":
    print('start dtu server.')
    dtu = TDtu()
    dtu.run()

    uvicorn.run(
        "dtuserver:app" ,
        host="0.0.0.0",
        port=12003 ,
        #reload= True
    )